import re
import requests
import argparse
import glob
import time
import sys
import subprocess
import os
import configparser

SCHEDULE_FILE_KEYWORD = ".schd"
PYTHON_FILE_KEYWORD = ".py"
SQL_FILE_KEYWORD = ".sql"

BASE_PROJECT_KEYWORD = "openGaussBase/"
RELIABLE_PROJECT_KEYWORD = "openGaussReliability/"

TEST_CASE_KEYWORD = "Opengauss_"

TEST_CASE_PATH_KEYWORD = "/testcase"
EXPECT_PATH_KEYWORD = "/expect"
SCHEDULE_PATH_KEYWORD = "/schedule"

ERROR_KEYWORD = "?.*ERROR"
EXPECT_ERROR_KEYWORD = ["-- @testpoint:", "合理报错"]

RESULT_SYMBOL_LIST = ["[-]", "[+]"]
STATUS_LIST = ["ok", "er", "sk"]
FILE_TYPE_DICT = {"schd": "s", "py": "d", "sql": "v"}


class CI:

    def __init__(self):
        """
        """
        self.pid = 0
        self.mid = 0
        self.token = ""
        self.git_test_case_path = ""
        self.error_msg = None

        self.modified_files_list = []
        self.checked_file_num = 0
        self.checked_files_error_message_dict = {}
        self.checked_files_status_code_dict = {}
        self.last_check_exec_time = 0
        self.result_string_length = 180

        self.cf = configparser.ConfigParser()
        self.start_time = time.time()

        self.return_code = 0

    def parse_command_line(self):
        parser = argparse.ArgumentParser()
        parser.description = "openGauss testcase CI"
        parser.add_argument('action', metavar="ActionMode", type=str, nargs=1)
        parser.add_argument("-d", "--ConfigPath", help="ConfigFilePath")
        config_file_path = parser.parse_args().ConfigPath

        self.cf.read(config_file_path)
        self.pid = self.cf.get("jenkins", "codehubMergeRequestTargetProjectId")
        self.mid = self.cf.get("jenkins", "codehubMergeRequestIid")
        self.token = self.cf.get("jenkins", "codehubAccessToken")
        self.git_test_case_path = self.cf.get("jenkins", "gitTestCasePath")

    def get_change_file_list(self):
        # 准备URL信息
        url = "https://codehub-y.huawei.com/api/v4/projects/%s/" \
              "merge_requests/%s/changes" % (self.pid, self.mid)
        headers = {
            "private-token": self.token
        }
        # 获取文件变动信息
        requests.packages.urllib3.disable_warnings()
        res = requests.get(url, headers=headers, verify=False)
        self.modified_files_list = res.json().get("changes")

    def print_end_string(self, test_point_string):
        if self.checked_file_num == 0:
            print("No %s file found." % test_point_string)
        else:
            total_check_exec_time = time.time() - self.start_time
            total_exec_time_string = self._get_exec_time(total_check_exec_time)
            now_time_string = time.strftime("%Y-%m-%d %H:%M:%S",
                                            time.localtime())
            passed_files_num = 0
            for key in self.checked_files_status_code_dict:
                if self.checked_files_status_code_dict[key] == 0:
                    passed_files_num += 1

            end_string = " Testing Result %d/%d Using Time %s At %s " % \
                         (passed_files_num, self.checked_file_num,
                          total_exec_time_string, now_time_string)
            end_string = end_string.center(self.result_string_length, "#")
            print(end_string)

    def get_check_result(self, file_path):
        file_check_status_code = self.checked_files_status_code_dict[file_path]

        symbol_string = RESULT_SYMBOL_LIST[self.checked_file_num % 2]
        now_time_string = time.strftime("[%Y-%m-%d %H:%M:%S]",
                                        time.localtime())
        exec_time_string = self._get_exec_time()
        file_type_string = "[%s]" % CI._get_file_type(file_path)
        file_path_string = file_path.rsplit(".", 1)[0]
        status_string = " : %s" % STATUS_LIST[file_check_status_code]
        result_string = (" ".join((symbol_string, now_time_string,
                                   exec_time_string, file_type_string,
                                   file_path_string)) + " ")
        check_string = result_string.ljust(self.result_string_length -
                                           len(status_string),
                                           ".") + status_string
        return check_string

    def print_check_result(self, file_path, test_point_string):
        # 打印检查结果Title
        if self.checked_file_num == 1:
            now_time_string = time.strftime("[%Y-%m-%d %H:%M:%S]",
                                            time.localtime())
            head_string = now_time_string. \
                center(len(now_time_string) + 2, " "). \
                center(self.result_string_length, "#")
            title_string = "Test point: %s" % test_point_string
            title_string = title_string.center(self.result_string_length, " ")
            print("%s\n%s" % (head_string, title_string))
        # 打印单条检查结果
        check_result = self.get_check_result(file_path)
        print(check_result)

    def _get_exec_time(self, last_check_exec_time=None):
        if not last_check_exec_time:
            last_check_exec_time = self.last_check_exec_time

        if last_check_exec_time < 100:
            exec_time_string = "[%06.3f s]" % last_check_exec_time
        elif last_check_exec_time <= 100 * 60:
            exec_time_string = "[%06.3f m]" % (last_check_exec_time / 60)
        elif last_check_exec_time <= 100 * 60 * 60:
            exec_time_string = "[%06.3f h]" % (last_check_exec_time / 60 / 60)
        else:
            exec_time_string = "[too long]"
        return exec_time_string

    def print_error_message(self):
        if self.return_code != 0:
            if self.error_msg:
                print(self.error_msg)
            for file_path in self.checked_files_status_code_dict:
                if self.checked_files_status_code_dict[file_path] == 1:
                    print("\n%s:" % file_path)
                    print(self.checked_files_error_message_dict[file_path])

    @staticmethod
    def is_test_case_file(file_path):
        if len(file_path.split("/")) > 2 \
            and TEST_CASE_PATH_KEYWORD in file_path \
            and TEST_CASE_KEYWORD in file_path.split("/")[-1] \
            and file_path.endswith((PYTHON_FILE_KEYWORD, SQL_FILE_KEYWORD)) \
            and file_path.startswith((BASE_PROJECT_KEYWORD,
                                      RELIABLE_PROJECT_KEYWORD)):
            return True
        else:
            return False

    @staticmethod
    def is_expect_file(file_path):
        if len(file_path.split("/")) > 2 \
            and EXPECT_PATH_KEYWORD in file_path \
            and TEST_CASE_KEYWORD in file_path.split("/")[-1] \
            and "." not in file_path \
            and file_path.startswith((BASE_PROJECT_KEYWORD,
                                      RELIABLE_PROJECT_KEYWORD)):
            return True
        else:
            return False

    @staticmethod
    def is_schedule_file(file_path):
        if len(file_path.split("/")) > 2 \
            and SCHEDULE_PATH_KEYWORD in file_path \
            and file_path.endswith(SCHEDULE_FILE_KEYWORD) \
            and file_path.startswith((BASE_PROJECT_KEYWORD,
                                      RELIABLE_PROJECT_KEYWORD)):
            return True
        else:
            return False

    @staticmethod
    def _get_file_type(file_path):
        file_type = "o"
        # sql、py、schd文件
        if file_path.split(".")[-1] in FILE_TYPE_DICT:
            file_type = FILE_TYPE_DICT.get(file_path.split(".")[-1])
        # 期望文件
        if "/expect/" in file_path \
            and "." not in file_path \
            and file_path.split("/")[-1].startswith(TEST_CASE_KEYWORD):
            file_type = "e"
        return file_type

    @staticmethod
    def create_file(file_path, msg):
        fd = open(file_path, "a")
        fd.write(msg)
        fd.close()

    @staticmethod
    def read_file(file_path):
        fd = open(file_path, "r")
        try:
            msg = fd.read()
            return msg
        finally:
            fd.close()

    @staticmethod
    def walk_file(dir_path):
        files_list = []
        for root, dirs, files in os.walk(dir_path):
            for file in files:
                files_list.append(os.path.join(root, file))
        return files_list


class CheckBaseTestCaseYat(CI):

    @staticmethod
    def run():
        ci = CheckBaseTestCaseYat()
        ci.parse_command_line()
        ci.get_change_file_list()
        ci._check_base_test_case_file_list()
        ci.print_error_message()
        sys.exit(ci.return_code)

    def _check_base_test_case_file_list(self):
        self.test_case_file_path_list = []
        self.temp_result_list = []
        self.yat_check_file_num = 0
        test_case_path = ""
        test_case_path_tmp = ""

        for file_changes in self.modified_files_list:
            if file_changes.get("deleted_file"):
                continue

            file_path = file_changes.get("new_path")
            # 仅处理基础功能部分的用例
            if file_path.startswith(BASE_PROJECT_KEYWORD):
                # 处理用例情况
                if CI.is_test_case_file(file_path):
                    test_case_path = file_path
                # 处理期望情况
                elif CI.is_expect_file(file_path):
                    test_case_path_tmp = file_path.replace(EXPECT_PATH_KEYWORD,
                                                       TEST_CASE_PATH_KEYWORD,
                                                       1) + SQL_FILE_KEYWORD
                    full_test_case_path = "%s/%s" % (self.git_test_case_path,
                                                     test_case_path_tmp)
                    if not os.path.isfile(full_test_case_path):
                        self.return_code = 1
                        self.checked_file_num += 1
                        error_message = "Cannot found testcase file in %s\n" \
                                        % test_case_path_tmp
                        self.checked_files_error_message_dict[file_path] \
                            = error_message
                        self.checked_files_status_code_dict[file_path] = 1
                        check_result = self.get_check_result(file_path)
                        self.temp_result_list.append(check_result)
                        continue
                else:
                    continue
                # 添加检查文件
                if test_case_path and test_case_path \
                    not in self.test_case_file_path_list:
                    self.yat_check_file_num += 1
                    self.checked_files_error_message_dict[test_case_path] = ""
                    self.test_case_file_path_list.append(test_case_path)

        print("yat_check_file_num:%d\n" % self.yat_check_file_num)
        print("checked_file_num:%d\n" % self.checked_file_num)
        if self.yat_check_file_num != 0 or self.checked_file_num != 0:
            self._run_yat()
        self.print_end_string("BaseTestCase")

    def _init_yat(self):
        self.yat_suite_path = self.cf.get("jenkins", "yatSuitePath")
        self.test_case_path = self.yat_suite_path + "/testcase"
        self.schd_file_path = "%s/schedule/openGaussTestCaseCI.schd" \
                              % self.yat_suite_path
        self.root_log_path = self.yat_suite_path + "/root.log"
        self.has_begin_string_print = False
        self.has_buffer_result_print = False

        core_path = self.cf.get("jenkins", "CORE_PATH")
        expect_path = self.yat_suite_path + "/expect"
        utils_path = self.git_test_case_path + "/openGaussBase/testcase/*"

        # 重置测试环境
        subprocess.getoutput("rm -rf %s %s %s %s" %
                             (self.test_case_path, expect_path,
                              self.schd_file_path, self.root_log_path))
        subprocess.getoutput("mkdir %s %s" %
                             (self.test_case_path, expect_path))
        subprocess.getoutput("cp -r %s %s" %
                             (utils_path, self.test_case_path))

        # 准备用例、期望和编排文件
        msg = ""
        for i in range(self.yat_check_file_num):
            source_test_case_file_path = "%s/%s" % \
                                         (self.git_test_case_path,
                                          self.test_case_file_path_list[i])
            source_expect_file_path = source_test_case_file_path.replace(
                TEST_CASE_PATH_KEYWORD, EXPECT_PATH_KEYWORD, 1).split(".")[0]
            target_test_case_file_path = "%s/%d.%s" % \
                                         (self.test_case_path, i,
                                          source_test_case_file_path.
                                          split(".")[1])
            target_expect_file_path = "%s/%d" % (expect_path, i)
            subprocess.getoutput("cp %s %s" % (source_test_case_file_path,
                                               target_test_case_file_path))
            subprocess.getoutput("cp %s %s" %
                                 (source_expect_file_path,
                                  target_expect_file_path))
            msg += "test:%d\n" % i
        CI.create_file(self.schd_file_path, msg)

        # 修改系统配置
        subprocess.getoutput("chmod 777 %s" % core_path)
        subprocess.getoutput("echo %s/core-%%e-%%p-%%t > "
                             "/proc/sys/kernel/core_pattern" % core_path)

    def _run_yat(self):
        self._init_yat()
        timeout_interval = self.cf.get("jenkins", "timeoutInterval")
        cmd = "ulimit -c unlimited; " \
              "yat suite run -d %s -s %s --timeout %s 2>/dev/null" % \
              (self.yat_suite_path, self.schd_file_path, timeout_interval)
        print(cmd)
        process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True,
                                   universal_newlines=True, bufsize=1)
        for line in iter(process.stdout.readline, ''):
            self._parse_and_print_yat_result(line)
        self._prepare_user_result()

    def _prepare_user_result(self):
        today_date_string = time.strftime("%m%d", time.localtime())
        user_id = self.cf.get("jenkins", "codehubMergedByUser")
        user_result_path = "%s/%s/rootlog/%s" % \
                           (self.cf.get("jenkins", "PUBLIC_DIR_PATH"),
                            today_date_string, user_id)
        user_root_log_path = "%s/root.log" % user_result_path
        remote_user_result_path = "%s/%s/rootlog/%s" % \
                                  (self.cf.get("jenkins", "PUBLIC_DIR_URL"),
                                   today_date_string, user_id)

        if self.return_code != 0:
            subprocess.getoutput("rm -rf %s" % user_result_path)
            subprocess.getoutput("mkdir -p %s" % user_result_path)
            if os.path.exists(self.root_log_path):
                subprocess.getoutput("iconv -f UTF-8 -t GB2312 %s > %s" %
                                     (self.root_log_path, user_root_log_path))
                self.error_msg = "There are some mistakes in the testcase. " \
                                 "You can check root.log on %s" % \
                                 remote_user_result_path

    def _parse_and_print_yat_result(self, yat_result):
        # 跳过空行
        if yat_result is None or len(yat_result.strip()) == 0:
            return
        # 打印缓冲区结果
        yat_result = yat_result.strip()
        if self.has_begin_string_print and not self.has_buffer_result_print:
            self.has_buffer_result_print = True
            if len(self.temp_result_list) > 0:
                print("\n".join(self.temp_result_list))
        # 校验yat结果——时间
        if re.match("[#]+\\s[0-9]{4}[0-9-: ]+[#]+", yat_result) \
            and not self.has_begin_string_print:
            head_string = yat_result.strip("#"). \
                center(self.result_string_length, "#")
            title_string = "Test point: BaseTestCase". \
                center(self.result_string_length, " ")
            self.has_begin_string_print = True
            print("%s\n%s" % (head_string, title_string))
        # 校验yat结果——Test Suite
        elif re.match("[\\s]+Test Suite:.*", yat_result):
            return
        # 校验yat结果——用例执行结果
        elif re.match("\\[[+-]\\].*: (er|ok|to)", yat_result):
            try:
                self.checked_file_num += 1
                index_key = int(yat_result.split(" ")[6])
                test_case_file = self.test_case_file_path_list[index_key]
                symbol_string = RESULT_SYMBOL_LIST[self.checked_file_num % 2]
                begin_string = "{0} {1}".format(symbol_string, " ".join(
                    yat_result.split(" ")[1:6]))
                file_path_string = test_case_file.rsplit(".", 1)[0]
                status_string = yat_result[-5:]
                result_string = (begin_string + " " + file_path_string +
                                 " ").ljust(self.result_string_length -
                                            len(status_string),
                                            ".") + status_string
                if self.has_begin_string_print:
                    print(result_string)
                else:
                    self.temp_result_list.append(result_string)

                if status_string.endswith("ok"):
                    self.checked_files_status_code_dict[test_case_file] = 0
                elif status_string.endswith("er"):
                    self.return_code = 1
                    self.checked_files_status_code_dict[test_case_file] = 1
                    self._get_error_message(test_case_file, index_key)
                else:
                    self.return_code = 1
                    self.checked_files_status_code_dict[test_case_file] = 2
            except (ValueError, IndexError) as reason:
                self.return_code = 1
                error_prompt = "\nCan not parse yat result: \"%s\"\n" \
                               "Reason: \"%s\"\n" % (yat_result, reason)
                print(error_prompt)

        # 校验yat结果——Test Result
        elif re.match("[#]+\\sTesting Result.*[#]+", yat_result):
            return
        else:
            return

    def _get_error_message(self, test_case_file, index_num):
        result_path = "%s/result/%d" % (self.yat_suite_path, index_num)
        if test_case_file.endswith(SQL_FILE_KEYWORD):
            result_path += ".diff"
        if os.path.exists(result_path):
            error_message = CI.read_file(result_path)
        else:
            expect_path = self.test_case_file_path_list[index_num]. \
                replace(TEST_CASE_PATH_KEYWORD, EXPECT_PATH_KEYWORD, 1). \
                split(".")[0]
            error_message = "Cannot found expect file in %s\n" % expect_path
        self.checked_files_error_message_dict[test_case_file] = error_message


class CheckSchedule(CI):

    @staticmethod
    def run():
        ci = CheckSchedule()
        ci.parse_command_line()
        ci.get_change_file_list()
        ci._check_schedule_file_list()
        ci.print_error_message()
        sys.exit(ci.return_code)

    def _check_schedule_file_list(self):
        has_scanned_project_list = []
        schd_path_list = []
        for file_changes in self.modified_files_list:
            file_path = file_changes.get("new_path")

            # 仅处理基础功能用例
            project_name = file_path.split("/")[0] + "/"
            if project_name != BASE_PROJECT_KEYWORD:
                continue

            # 处理删除用例，未同步编排文件的情况
            if file_changes.get("deleted_file") \
                and CI.is_test_case_file(file_path) \
                and project_name not in has_scanned_project_list:
                has_scanned_project_list.append(project_name)
                schd_dir_path = "%s/%s%s" % (self.git_test_case_path,
                                             file_path.rsplit("/", 1)[0].
                                             split(TEST_CASE_PATH_KEYWORD)[0],
                                             SCHEDULE_PATH_KEYWORD)
                scan_path_list = CI.walk_file(schd_dir_path)
                for scan_path in scan_path_list:
                    relative_path = scan_path.replace(self.git_test_case_path
                                                      + "/", "")
                    if CI.is_schedule_file(relative_path) \
                        and relative_path not in schd_path_list:
                        schd_path_list.append(relative_path)

            # 若更新文件为编排文件，则检查该文件
            elif not file_changes.get("deleted_file") \
                and CI.is_schedule_file(file_path) \
                and file_path not in schd_path_list:
                schd_path_list.append(file_path)
            else:
                continue

        for schd_path in schd_path_list:
            self.checked_file_num += 1
            self.checked_files_error_message_dict[schd_path] = ""
            start_time = time.time()
            self._check_schedule_file(schd_path)
            self.last_check_exec_time = time.time() - start_time
            self.print_check_result(schd_path, "schedule")

        self.print_end_string("schedule")

    def _check_schedule_file(self, file_path):
        self.checked_files_status_code_dict[file_path] = 0
        schedule_file_path = self.git_test_case_path + "/" + file_path
        with open(schedule_file_path, "r", encoding='utf-8') as fd:
            for (num, line) in enumerate(fd):
                # 跳过编排文件中注释项
                if line.startswith("#"):
                    continue
                # 检查编排文件格式
                if line.find(" ") != -1:
                    error_message = "Error: The %dth line '%s' in the file " \
                                    "'%s' contains spaces.\n" % \
                                    (num + 1, line, file_path)
                    self.checked_files_error_message_dict[file_path] += \
                        error_message
                    self.checked_files_status_code_dict[file_path] = 1
                test_case_path = "%s/%s/testcase/" % \
                                 (self.git_test_case_path,
                                  file_path.split("/")[0])
                # 检查编排文件中用例路径
                if line.startswith("test:"):
                    test_case_path += line.split(":")[1].strip() + ".*"
                else:
                    test_case_path += line.strip() + ".*"
                if len(glob.glob(test_case_path)) != 1:
                    error_message = "Error: Can not found the file '%s' in " \
                                    "line %d.\n" % (line.strip(), num + 1)
                    self.checked_files_error_message_dict[file_path] += \
                        error_message
                    self.checked_files_status_code_dict[file_path] = 1
        if self.checked_files_status_code_dict[file_path] == 1:
            self.return_code = 1


class CheckFormat(CI):

    @staticmethod
    def run():
        ci = CheckFormat()
        ci.parse_command_line()
        ci.get_change_file_list()
        ci._check_test_case_and_expect_file_list()
        ci.print_error_message()
        sys.exit(ci.return_code)

    def _check_test_case_and_expect_file_list(self):
        for file_changes in self.modified_files_list:
            if file_changes.get("deleted_file"):
                continue
            file_path = file_changes.get("new_path")
            start_time = time.time()
            if CI.is_test_case_file(file_path):
                self._check_test_case_format(file_path)
            elif CI.is_expect_file(file_path):
                self._check_expect_format(file_path)
            else:
                continue
            if self.checked_files_status_code_dict[file_path] == 1:
                self.return_code = 1
            self.checked_file_num += 1
            self.last_check_exec_time = time.time() - start_time
            self.print_check_result(file_path, "format")
        self.print_end_string("format")

    def _check_test_case_format(self, file_path):
        template_file_path = sys.path[0] + "/template"
        self.checked_files_status_code_dict[file_path] = 0
        self.checked_files_error_message_dict[file_path] = ""
        if file_path.endswith(SQL_FILE_KEYWORD):
            template_file_path += SQL_FILE_KEYWORD
        elif file_path.endswith(PYTHON_FILE_KEYWORD):
            template_file_path += PYTHON_FILE_KEYWORD
        else:
            return

        test_case_file_path = "%s/%s" % (self.git_test_case_path, file_path)
        check_key_list = CI.read_file(template_file_path).split("\n")
        check_key_index = 0
        with open(test_case_file_path, "r", encoding='utf-8') as fd:
            for (num, line) in enumerate(fd):
                if check_key_index >= len(check_key_list):
                    break
                if line.startswith(" "):
                    continue

                if not line.startswith(check_key_list[check_key_index]):
                    error_message = "Error: The %dth line is invalid.\n" \
                                    "Your line:\n[%s]\nExpect:\n[%s]\n" % \
                                    (num + 1, line.rstrip("\n"),
                                     check_key_list[check_key_index])
                    self.checked_files_status_code_dict[file_path] = 1
                    self.checked_files_error_message_dict[file_path] += \
                        error_message
                check_key_index += 1

    def _check_expect_format(self, file_path):
        expect_file_path = "%s/%s" % (self.git_test_case_path, file_path)
        error_flag = False
        self.checked_files_status_code_dict[file_path] = 0
        self.checked_files_error_message_dict[file_path] = ""
        with open(expect_file_path, "r", encoding='utf-8') as fd:
            for (num, line) in enumerate(fd):
                if line.startswith(EXPECT_ERROR_KEYWORD[0]) \
                    and EXPECT_ERROR_KEYWORD[1] in line:
                    error_flag = True
                if line.startswith(ERROR_KEYWORD) and not error_flag:
                    error_message = "Error: The %dth line '%s' contains '%s'" \
                                    " but '%s' is not found in '%s'\n" % \
                                    (num + 1, line.rstrip("\n"), ERROR_KEYWORD,
                                     EXPECT_ERROR_KEYWORD[1],
                                     EXPECT_ERROR_KEYWORD[0])
                    self.checked_files_status_code_dict[file_path] = 1
                    self.checked_files_error_message_dict[file_path] += \
                        error_message


if __name__ == '__main__':
    actionMode = sys.argv[1]
    if actionMode == "schd":
        CheckSchedule.run()
    elif actionMode == "base":
        CheckBaseTestCaseYat.run()
    elif actionMode == "format":
        CheckFormat.run()
    else:
        print("Only support check [schd, base, format] now.")
